package com.ra.model.deal;

import java.util.Date;
import java.util.List;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Set;
import java.util.HashSet;

import javax.persistence.Id;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.OneToMany;

import com.ra.model.user.UserProfile;
import com.ra.model.merchant.Merchant;

@Entity
@Table(name = "BID")
public class Bid {
	/*
	 * A Bid encapsulates a bid as well as a deal
	 * after the user has accepted it as such.
	 */
	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	@Column(name = "BID_ID")
	private Integer id;

	public int getId() {
		return id;
	}
	
	@Column(name = "CREATION_DATE")
    private Date creationDate;

	public void setCreationDate(Date creationDate) {
		this.creationDate = creationDate;
	}
	
	public Date getCreationDate() {
	    return creationDate;	
	}
	
	@Column(name = "EXPIRATION_DATE")
	private Date expirationDate;
	
	public void setExpirationDate(Date expirationDate) {
		this.expirationDate = expirationDate;
	}
	
	public Date getExpirationDate() {
		return expirationDate;
	}
	
	@ManyToOne(targetEntity = com.ra.model.deal.Ask.class)
	@JoinColumn(name = "ASK_ID", nullable = false)
	private Ask ask;
	
	public void setAsk(Ask ask) {
		this.ask = ask;
	}
	
	public Ask getAsk() {
		return ask;
	}
	
	/*
	 * The merchant may give additional product/service specifications
	 * or offer accessories with the product etc. Record them here.
	 */
	@Column(name="BID_DESCRIPTION")
	private String bidDescription;
	
	public void setBidDescription(String bidDescription) {
		this.bidDescription = bidDescription;
	}
	
	public String getBidDescription() {
		return bidDescription;
	}
	
	@ManyToOne(targetEntity = com.ra.model.merchant.Merchant.class)
	@JoinColumn(name = "MERCHANT_ID", nullable = false)
	private Merchant bidder;
	
	public void setBidder(Merchant merchant) {
		this.bidder = merchant;
	}
	
	public Merchant getBidder(Merchant merchant) {
	    return bidder;	
	}
	
	/*
	 * Turn on this bit when a bid turns into a deal
	 */
	@Column(name = "IS_DEAL")
	private Boolean isDeal = false;
	
	public void setIsDeal(Boolean isDeal) {
		this.isDeal = isDeal;
	}
	
	public Boolean getIsDeal() {
		return isDeal;
	}

	/*
	 * If we allow merchants to edit their bids then we are going to have 
	 * multiple versions of the same. The following methods provide a way 
	 * to manipulate the different versions of a bid.
	 * 
	 * allDealSlabs stores all different versions of the bid in a sorted 
	 * order - from current version to previous versions in chronological
	 * order. For example the first 4 elements in allDealSlabs could be 4
	 * slabs in the current version of the bid sorted by the min qty in each
	 * slab. The next 3 elements could be the 3 deal slabs in the previous 
	 * version of the bid again sorted by the min qty in each slab.
	 */
	@org.hibernate.annotations.CollectionOfElements(targetElement = com.ra.model.deal.DealSlab.class)
	@JoinTable(name = "BID_DEAL_SLABS", joinColumns = @JoinColumn(name = "BID_ID"))
	@org.hibernate.annotations.IndexColumn(name = "SLAB_ORDER", base = 1)
	private List<DealSlab> allDealSlabs = new LinkedList<DealSlab>();
	
	@Transient
	private Deal currentDeal = null;
	
	public synchronized Deal getCurrentDeal() {
		// If we have already cached the current deal then return it
		if (currentDeal != null) return currentDeal;
		
		// Sanity check: No bid is possible without deal slabs
		assert(allDealSlabs.size() > 0);
		
		// Iterate through all the deal slabs looking for slabs
		// with the latest version. 
		int fromIndex = 0;
		int toIndex = 1;
		int currentVersion = allDealSlabs.get(0).getVersion();
	    for (DealSlab slab: allDealSlabs.subList(1, allDealSlabs.size())) {
	    	/* The slabs are arranged in order with the latest
	    	 * version first. Iterate over the list of deal slabs
	    	 * until we find a deal slab with version less than the
	    	 * currentVersion
	    	 */ 
	    	if (currentVersion > slab.getVersion()) break;
	        toIndex += 1;
	    }
	    
	    currentDeal = new Deal(allDealSlabs.subList(fromIndex, toIndex));
	    return currentDeal;
	}
	
    public synchronized void setCurrentDeal(Deal currentDeal) {
    	int nextVersion = (allDealSlabs.size() > 0) ? 
    	    allDealSlabs.get(0).getVersion() + 1: 1;
    	
    	List<DealSlab> dealSlabs = currentDeal.getDealSlabs();
        for (int index = 0; index < dealSlabs.size(); index++) {
    		dealSlabs.get(index).setVersion(nextVersion);
    		allDealSlabs.add(index, dealSlabs.get(index));
    	}
        
        // invalidate the cache
        allDeals = null;
        previousDeals = null;
        this.currentDeal = null; 
    }
	
    @Transient
    List<Deal> previousDeals = null;
    
    public synchronized List<Deal> getPreviousDeals() {
    	// If we have already computed the previous deals then return it
    	if (previousDeals != null) return previousDeals;
    	
    	// Get the current deal to figure out how many deal slabs
    	// it has. It the total number of deal slabs is the same as
    	// the number of current deal slabs then there are no previous 
    	// deals. Return an empty list of previous deals.
    	int fromIndex = getCurrentDeal().getDealSlabs().size();
    	if (allDealSlabs.size() == fromIndex) return new ArrayList<Deal>();
    	int toIndex = fromIndex;
    	
        int version = -1;
        for (DealSlab dealSlab: allDealSlabs.subList(fromIndex, allDealSlabs.size())) {
        	if (version > dealSlab.getVersion()) {
           		if (previousDeals == null) previousDeals = new ArrayList<Deal>();
        		previousDeals.add(new Deal(allDealSlabs.subList(fromIndex, toIndex)));
        		fromIndex = toIndex;
        	}
        		
        	toIndex += 1;
        }
       
        if (toIndex > fromIndex) {
        	if (previousDeals == null) previousDeals = new ArrayList<Deal>();
            previousDeals.add(new Deal(allDealSlabs.subList(fromIndex, toIndex)));
        }
        
        return (previousDeals == null) ? new ArrayList<Deal>() : previousDeals;
    }
    
    @Transient
    List<Deal> allDeals = null;
    
    public List<Deal> getAllDeals() {
    	if (allDeals != null) return allDeals;
    	allDeals = new ArrayList<Deal>();
    	allDeals.add(getCurrentDeal());
    	List<Deal> allPreviousDeals = getPreviousDeals();
    	if (allPreviousDeals.size() > 0) allDeals.addAll(allPreviousDeals);
    	return allDeals;
    }
	
	/*
	 * List of participants for this deal
	 */
	@OneToMany
	@JoinTable(name = "DEAL_PARTICIPANTS",
			   joinColumns = @JoinColumn(name = "BID_ID"),
			   inverseJoinColumns = @JoinColumn(name = "USER_ID"))
	private Set<UserProfile> dealParticipants = new HashSet<UserProfile>();
	
	public void setDealParticipants(Set<UserProfile> dealParticipants) {
		this.dealParticipants = dealParticipants;
	}
	
	public Set<UserProfile> getDealParticipants() {
		return dealParticipants;
	}
	
	public void addDealParticipant(UserProfile user) {
		dealParticipants.add(user);
	}
	
	/*
	 * Payment details - There will be a logic to calculate the 
	 * advance payment based on the prices in the deal slab
	 */
	@Column(name = "ADVANCE_PAYMENT")
	private Float advance_payment;
	
	public void setAdvancePayment(Float advance_payment) {
		this.advance_payment = advance_payment;
	}
	
	public float getAdvancePayment() {
		return advance_payment;
	}
}